<?php
defined('_JEXEC') or die('Restricted access');

/**
 * Database tablename: tbl_sessions
 * cols:
 *   id               bigint(20)   PrimaryKey
 *   session_id       varchar(255), Unique
 *   date_created     datetime, Default 0000-00-00 00:00:00 
 *   session_data     longtext, Default NULL
 *   timestamp        timestamp, Index, CURRENT_TIMESTAMP 
 */

/**
 * Description of Session
 *
 * @todo  fingerprint or something else, for more security (eg.to prevent CSRF attacks, etc.)
 * 
 * @author George Zeakis<zeageorge@google.com>
 */
class Session {
    
    /**
     * @var DatabaseAdapterInterface
     */
    protected $_db = NULL;

    /**
     *
     * @var array
     */
    protected $_vars = NULL;

    /**
     * @param array $dbData An array like: 'DB_TYPE'=>'mysql',
     *                                     'DB_HOST'=>'localhost',
     *                                     'DB_PORT'=>'3306',
     *                                     'DB_NAME'=>'pfp_01',
     *                                     'DB_USER'=>'pfp_01',
     *                                     'DB_PASS'=>'1234',
     *                                     'TABLE_PREFIX'=> 'tbl_',
     *                                     'DB_TABLE'=>'sessions',
     * @param string $session_id Default=NULL
     * @return Session Returns Session object
     */
    public function __construct(array $dbData, $session_id=NULL) {
        $tmp = __FUNCTION__."_tmp"; if (isset($this->_vars["$tmp"])){ unset($this->_vars["$tmp"]); }
        try {
            $this->registerEvents();
            if (!session_start()){throw new Exception("Could not Start Session");}
            
            $this->_vars = array('dbData'=>$dbData,);
            $this->_vars["session_record"] = new dynamicContainer();
            
            $temp_session_id = is_null($session_id) ? (string)session_id() : (string)$session_id;
            Tools::isValidSessionID($temp_session_id, 26, 128, TRUE);
            
            $this->_vars["dbTable"] = $this->_vars["dbData"]["TABLE_PREFIX"].$this->_vars["dbData"]["DB_TABLE"];
            $this->_db = new DatabaseAdapter();
            
            $temp_array = $this->get_session_record_from_database($temp_session_id);
            foreach ($temp_array as $key => $value) {
                $this->_vars["session_record"]->{$key} = (string)$value;
            }
            
            if (!isset($this->_vars["session_record"]->id)) { 
                $this->create_new_session_record_into_database(array(
                    'session_id'=>$temp_session_id,
                    'date_created'=>date ("Y-m-d H:i:s", time()),
                    'session_data'=>(string)json_encode(array()),
                ));
                
                $temp_array = $this->get_session_record_from_database($temp_session_id);
                foreach ($temp_array as $key => $value) {
                    $this->_vars["session_record"]->{$key} = (string)$value;
                }
                if (!isset($this->_vars["session_record"]->id)) {throw new Exception("Unknown state. Could not get the new Session data from database.");}
            }
            unset($temp_array, $temp_session_id);
            
            if (isset($this->_vars["session_record"]->session_data)){ 
                $this->_vars["session_record"]->session_data =(array)json_decode($this->_vars["session_record"]->session_data, TRUE, 512);
            }
        } catch (Exception $exc) {
            // error/Exception handler
        }
    }

    private function registerEvents(){
        $this->_events = array();
    }

    /**
     * @param string $session_id 
     * @param string $session_data_string 
     * @return boolean Returns TRUE on success
     */
    private function save_session_data_to_database($session_id, $session_data_string){
        $tmp = __FUNCTION__."_tmp"; if (isset($this->_vars["$tmp"])){ unset($this->_vars["$tmp"]); }
        try {
            Tools::isValidSessionID($session_id, 26, 128, TRUE);
            $this->_vars["$tmp"]["sessionData_toSave"] = array('session_data'=>(string)$session_data_string);
            $this->_vars["$tmp"]["session_id"] = (string)$session_id;

            $this->_db->connect($this->_vars["dbData"]);
            $this->_vars["$tmp"]["save_session_results"] = 
            $this->_db->update($this->_vars["dbTable"], $this->_vars["$tmp"]["sessionData_toSave"], "`session_id`='".$this->_vars["$tmp"]["session_id"]."'"); 
            $this->_db->disconnect();
            unset($this->_vars["$tmp"]);
            return TRUE;
        } catch (Exception $exc) {
            // error/Exception handler
        }
    }

    /**
     * It will try to totaly destroy the session....
     * after that, it will regenerate the session_id
     * @param string $session_id
     * @return boolean Returns TRUE on success
     */
    private function delete_session_record_from_database($session_id) {
        $tmp = __FUNCTION__."_tmp"; if (isset($this->_vars["$tmp"])){ unset($this->_vars["$tmp"]); }
        try{
            Tools::isValidSessionID($session_id, 26, 128, TRUE);
            $this->_vars["$tmp"]["session_id"] = (string)$session_id;

            @session_write_close(); @session_destroy(); @session_unset(); @setcookie(session_name(),'',0,'/'); 
            $_SESSION = array(); unset($_SESSION); @session_regenerate_id(true);
            
            $this->_db->connect($this->_vars["dbData"]);
            $this->_vars["$tmp"]["Delete_Session_Results"] = $this->_db->delete_all($this->_vars["dbTable"], "`id`='".$this->_vars["$tmp"]["session_id"]."'");
            $this->_db->disconnect();
            unset($this->_vars["$tmp"]);
            return TRUE;
        } catch (Exception $exc) {
           // error/Exception handler
        }
    }
    
    /**
     * @param string $session_id 
     * @return array Returns Assoc array with session data
     */
    private function get_session_record_from_database($session_id){
        $tmp = __FUNCTION__."_tmp"; if (isset($this->_vars["$tmp"])){ unset($this->_vars["$tmp"]); }
        try {
            Tools::isValidSessionID($session_id, 26, 128, TRUE);
            $this->_vars["$tmp"]["sqlQuery"] = "SELECT * FROM `".$this->_vars["dbTable"]."` WHERE `session_id`=:s_id LIMIT 1";
            $this->_vars["$tmp"]["bindParams"] = array(':s_id'=> (string)$session_id);
            
            $this->_db->connect($this->_vars["dbData"]);
            $this->_vars["$tmp"]["select_session_results"] = $this->_db->select($this->_vars["$tmp"]["sqlQuery"], $this->_vars["$tmp"]["bindParams"]);
            $this->_db->disconnect();
            
            $this->_vars["$tmp"]["return_results"] = array();
            if (count($this->_vars["$tmp"]["select_session_results"])=='1'){
                foreach ($this->_vars["$tmp"]["select_session_results"][0] as $key => $value) {
                    $this->_vars["$tmp"]["return_results"]["$key"] = (string)$value;}
            }
            $re = $this->_vars["$tmp"]["return_results"]; unset($this->_vars["$tmp"]);
            return $re;
        } catch (Exception $exc) {
           // error/Exception handler
        }
    }

    /**
     * Regenerates the session_id, it will also update the database and the internal vars
     * 
     * @param boolean $delete_old_session 
     * @return boolean True on success
     */
    private function regenerate_id_into_database($delete_old_session=FALSE){
        $tmp = __FUNCTION__."_tmp"; if (isset($this->_vars["$tmp"])){ unset($this->_vars["$tmp"]); }
        try {
            $this->_vars["$tmp"]["old_sessionID_of_PHP_Active_Session"] = $this->_vars["session_record"]->session_id;
            $this->_vars["$tmp"]["delete_old_session"] = (boolean)$delete_old_session;
            
            $found = TRUE; $num_of_try = 0;
            while ($found) {
                session_regenerate_id($this->_vars["$tmp"]["delete_old_session"]);
                $this->_vars["$tmp"]["new_sessionID_of_PHP_Active_Session"] = session_id();
                Tools::isValidSessionID($this->_vars["$tmp"]["new_sessionID_of_PHP_Active_Session"], 26, 128, TRUE);
                if ($this->session_id_exists_in_database($this->_vars["$tmp"]["new_sessionID_of_PHP_Active_Session"])){ $num_of_try++; }else{ $found = FALSE; }
                if ($num_of_try>5){  //could not regenerate_id
                    // I think, if we want to reset the session_id, the session must be closed ?? (eg. before calling session_start() ??
                    // should I close the session, reset it and then reopen it?
                    session_id($this->_vars["$tmp"]["old_sessionID_of_PHP_Active_Session"]);
                    unset($this->_vars["$tmp"]); 
                    throw new Exception("Could not regenerate Session ID");
                }
            }
            $this->set_session_id_to_database($this->_vars["$tmp"]["new_sessionID_of_PHP_Active_Session"], $this->_vars["$tmp"]["old_sessionID_of_PHP_Active_Session"]);
            unset($this->_vars["$tmp"]);
            return TRUE;
        } catch (Exception $exc) {
           // error/Exception handler
        }
    }
    
    /**
     * 
     * @param string $session_id
     * @return boolean Returns TRUE if $session_id already exists in database, or FALSE if it doesn't exists
     */
    private function session_id_exists_in_database($session_id){
        $tmp = __FUNCTION__."_tmp"; if (isset($this->_vars["$tmp"])){ unset($this->_vars["$tmp"]); }
        try {
            $this->_vars["$tmp"]["session_id_toCheck"] = (string)$session_id;
            $this->_vars["$tmp"]["sqlQuery"]   = "SELECT COUNT(session_id) AS C FROM `".$this->_vars["dbTable"]."` WHERE `session_id`=:s_id LIMIT 1";
            $this->_vars["$tmp"]["bindParams"] = array(':s_id'=>$this->_vars["$tmp"]["session_id_toCheck"]);
            
            $this->_db->connect($this->_vars["dbData"]);
            $this->_vars["$tmp"]["session_id_exists_selectResults"] = $this->_db->select($this->_vars["$tmp"]["sqlQuery"], $this->_vars["$tmp"]["bindParams"]);
            $this->_db->disconnect();
            
            if (isset($this->_vars["$tmp"]["session_id_exists_selectResults"][0]['C'])){
                if ($this->_vars["$tmp"]["session_id_exists_selectResults"][0]['C']=='0')
                    { $this->_vars["$tmp"]["session_id_exists_Results"] = FALSE; } else { $this->_vars["$tmp"]["session_id_exists_Results"] = TRUE;}
            } else { 
                throw new Exception("Unknown state: Could not check if Session ID exists in Database"); 
            }
            $re = $this->_vars["$tmp"]["session_id_exists_Results"];
            unset($this->_vars["$tmp"]);
            return $re;
        } catch (Exception $exc) {
           // error/Exception handler
        }
    }

    /**
     * Updates the database with the new session_id (session_id)
     * 
     * @param type $new_session_id
     * @param type $old_session_id
     * @return boolean TRUE on success
     */
    private function set_session_id_to_database($new_session_id, $old_session_id){
        $tmp = __FUNCTION__."_tmp"; if (isset($this->_vars["$tmp"])){ unset($this->_vars["$tmp"]); }
        try {
            $this->_vars["$tmp"]["new_session_id"] = $new_session_id;
            $this->_vars["$tmp"]["old_session_id"] = $old_session_id;
            
            $this->_db->connect($this->_vars["dbData"]);
            $this->_vars["$tmp"]["set_new_session_id_Results"] = 
            $this->_db->update($this->_vars["dbTable"],array('session_id'=>$this->_vars["$tmp"]["new_session_id"]), "`session_id`='".$this->_vars["$tmp"]["old_session_id"]."'");
            $this->_db->disconnect();
            unset($this->_vars["$tmp"]);
            return TRUE;
        } catch (Exception $exc) {
           // error/Exception handler
        }
    }
    
    /**
     * @param array $new_session_record_data An array like: 
     *                                           array(
     *                                               'session_id'=>$temp_session_id,
     *                                               'date_created'=>date ("Y-m-d H:i:s", time()),
     *                                               'session_data'=>(string)json_encode(array()),
     *                                           )
     * @return boolean Returns TRUE on success
     */
    private function create_new_session_record_into_database(array $new_session_record_data){
        $tmp = __FUNCTION__."_tmp"; if (isset($this->_vars["$tmp"])){ unset($this->_vars["$tmp"]); }
        try {
            $this->_vars["$tmp"]["dataToInsert"] = $new_session_record_data; 
            
            $this->_db->connect($this->_vars["dbData"]);
            $this->_vars["$tmp"]["insert_results"] = $this->_db->insert($this->_vars["dbTable"], $this->_vars["$tmp"]["dataToInsert"]);
            $this->_db->disconnect();
            unset($this->_vars["$tmp"]);
            return TRUE;
        } catch (Exception $exc) {
           // error/Exception handler
        }
    }
    
    public function __get($name){
        if(isset($this->_vars["session_record"]->session_data[$name])){
          return $this->_vars["session_record"]->session_data[$name];
        } else { return NULL; }
    }
    public function __set($name,$value){ 
        // the following must be this way,
        // because we get an error: PHP Notice: Indirect modification of overloaded property... 
        // if we do it the "normal" way, eg. $this->_vars["session_record"]->session_data[$name] = $value;
        $yu = $this->_vars["session_record"]->session_data; 
        $yu[$name] = $value;
        $this->_vars["session_record"]->session_data = $yu; 
    }
    public function __isset($name){ return isset($this->_vars["session_record"]->session_data[$name]); }
    public function __unset($name){ 
        // the following must be this way,
        // because we get an error: PHP Notice: Indirect modification of overloaded property... 
        // if we do it the "normal" way, eg. unset($this->_vars["session_record"]->session_data[$name]);
        $yu = $this->_vars["session_record"]->session_data;
        unset($yu[$name]);
        $this->_vars["session_record"]->session_data = $yu; 
    }
    
    public function save(){
        $tmp = __FUNCTION__."_tmp"; if (isset($this->_vars["$tmp"])){ unset($this->_vars["$tmp"]); }
        try {
            if (!isset($this->_vars["session_record"]->session_id)){
                throw new Exception("Could not save session data into database, session id is not defined");
            }
            $this->save_session_data_to_database($this->_vars["session_record"]->session_id, 
                    (string)json_encode($this->_vars["session_record"]->session_data));
            
        } catch (Exception $exc) {
           // error/Exception handler
        }
    }
    
    public function delete(){
        $tmp = __FUNCTION__."_tmp"; if (isset($this->_vars["$tmp"])){ unset($this->_vars["$tmp"]); }
        try {
            if (!isset($this->_vars["session_record"]->session_id)){
                throw new Exception("Could not delete session from database, session id is not defined");
            }
            $this->delete_session_record_from_database($this->_vars["session_record"]->session_id);
        } catch (Exception $exc) {
           // error/Exception handler
        }
    }
    
    public function regenerate_id($delete_old_session){
        $tmp = __FUNCTION__."_tmp"; if (isset($this->_vars["$tmp"])){ unset($this->_vars["$tmp"]); }
        try {
            if (!isset($this->_vars["session_record"]->session_id)){
                throw new Exception("Could not regenerate session id, session id is not defined");
            }
            $this->regenerate_id_into_database($delete_old_session);
        } catch (Exception $exc) {
           // error/Exception handler
        }
    }

    public function sessionRecord_toArray() {
        return (array)$this->_vars["session_record"]->toArray();
    }
    
    public function session_data_toArray(){
        return (array)$this->_vars["session_record"]->session_data;
    }

    public function __destruct() { }

}